package cn.iocoder.yudao.module.pay.wallet.service;

import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.business.basic.dao.repository.LockRedisRepository;
import cn.iocoder.yudao.framework.common.exception.Assertions;
import cn.iocoder.yudao.framework.common.exception.KnownServiceException;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.module.api.pay.bill.enums.BillCategoryEnum;
import cn.iocoder.yudao.module.api.pay.bill.enums.BillTypeEnum;
import cn.iocoder.yudao.module.api.pay.wallet.enums.PayWalletBizTypeEnum;
import cn.iocoder.yudao.module.pay.bill.service.PayBillService;
import cn.iocoder.yudao.module.pay.order.dal.dataobject.PayOrderDO;
import cn.iocoder.yudao.module.pay.order.service.PayOrderService;
import cn.iocoder.yudao.module.pay.refund.dal.dataobject.PayRefundDO;
import cn.iocoder.yudao.module.pay.refund.service.PayRefundService;
import cn.iocoder.yudao.module.pay.transfer.dal.dataobject.PayTransferDO;
import cn.iocoder.yudao.module.pay.transfer.service.PayTransferService;
import cn.iocoder.yudao.module.pay.wallet.bo.WalletTransactionCreateReqBO;
import cn.iocoder.yudao.module.pay.wallet.dal.dataobject.PayWalletDO;
import cn.iocoder.yudao.module.pay.wallet.dal.dataobject.PayWalletTransactionDO;
import cn.iocoder.yudao.module.pay.wallet.dal.mysql.PayWalletMapper;
import cn.iocoder.yudao.module.pay.wallet.vo.PayWalletPageReqVO;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.concurrent.atomic.AtomicReference;

import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.api.pay.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.api.pay.wallet.enums.PayWalletBizTypeEnum.*;

/**
 * 钱包 Service 实现类
 *
 * @author jason
 */
@Service
@Slf4j
public class PayWalletServiceImpl implements PayWalletService {
    private static final long GET_TIMEOUT_MILLIS = 10 * DateUtils.SECOND_MILLIS;
    private static final long UPDATE_TIMEOUT_MILLIS = 120 * DateUtils.SECOND_MILLIS;
    private static final String PAY_WALLET_GET_LOCK = "xianmin:pay:wallet:get-lock:%d%d";
    /**
     * 支付钱包更新的分布式锁
     */
    private static final String PAY_WALLET_UPDATE_LOCK = "xianmin:pay:wallet:update-lock:%d";

    @Resource
    private PayWalletMapper payWalletMapper;
    @Resource
    private LockRedisRepository lockRedisRepository;

    @Resource
    @Lazy // 延迟加载，避免循环依赖
    private PayWalletTransactionService payWalletTransactionService;
    @Resource
    @Lazy // 延迟加载，避免循环依赖
    private PayOrderService payOrderService;
    @Resource
    @Lazy // 延迟加载，避免循环依赖
    private PayRefundService payRefundService;
    @Resource
    @Lazy // 延迟加载，避免循环依赖
    private PayTransferService payTransferService;
    @Resource
    @Lazy // 延迟加载，避免循环依赖
    private PayBillService payBillService;


    @Override
    public PayWalletDO getOrCreateWallet(Long userId, Integer userType) {
        return lockRedisRepository.lock(String.format(PAY_WALLET_GET_LOCK, userType, userId), GET_TIMEOUT_MILLIS, () -> {
            // 需要上锁，不然会出现并发问题，数据库里同一个用户可能创建有两条数据
            PayWalletDO wallet = payWalletMapper.selectByUserIdAndType(userId, userType);
            if (wallet == null) {
                wallet = new PayWalletDO();
                wallet.setUserType(userType);
                wallet.setUserId(userId);
                wallet.setBalance(0);
                wallet.setMoney(0);
                wallet.setMoneyCashing(0);
                wallet.setFreezePrice(0);
                wallet.setTotalExpense(0);
                wallet.setTotalRecharge(0);
                wallet.setCreateTime(LocalDateTime.now());
                payWalletMapper.insert(wallet);
            }
            return wallet;
        });
    }

    @Override
    public PayWalletDO getWallet(Long walletId) {
        return payWalletMapper.selectById(walletId);
    }

    @Override
    public PageResult<PayWalletDO> getWalletPage(PayWalletPageReqVO pageReqVO) {
        return payWalletMapper.selectPage(pageReqVO);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public PayWalletTransactionDO orderPay(Long walletId, String payNo, Integer price) {
        // 1. 判断付款单据是否存
        PayOrderDO orderDO = payOrderService.getOrderByNo(payNo);
        if (orderDO == null) {
            throw exception(PAY_ORDER_NOT_FOUND);
        }
        PayWalletDO wallet = payWalletMapper.selectById(walletId);
        Assertions.notNull(wallet, "钱包不存在：" + walletId);
        // 2. 扣减余额
        return reduceWalletBalance(wallet.getId(), String.valueOf(orderDO.getId()), PAYMENT, price);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public PayWalletTransactionDO orderRefund(String refundNo, Integer refundPrice, String reason) {
        // 1.1 判断退款单是否存在
        PayRefundDO payRefund = payRefundService.getRefundByNo(refundNo);
        if (payRefund == null) {
            throw exception(REFUND_NOT_FOUND);
        }
        // 1.2 校验是否可以退款
        Long walletId = validateWalletCanRefund(payRefund.getId(), payRefund.getChannelOrderNo());
        PayWalletDO wallet = payWalletMapper.selectById(walletId);
        Assert.notNull(wallet, "钱包 {} 不存在", walletId);

        // 2. 增加余额
        return addWalletBalance(walletId, String.valueOf(payRefund.getId()), PAYMENT_REFUND, refundPrice);
    }

    /**
     * 校验是否能退款
     *
     * @param refundId 支付退款单 id
     * @param walletPayNo 钱包支付 no
     */
    private Long validateWalletCanRefund(Long refundId, String walletPayNo) {
        // 1. 校验钱包支付交易存在
        PayWalletTransactionDO walletTransaction = payWalletTransactionService.getWalletTransactionByNo(walletPayNo);
        if (walletTransaction == null) {
            throw exception(WALLET_TRANSACTION_NOT_FOUND);
        }
        // 2. 校验退款是否存在
        PayWalletTransactionDO refundTransaction = payWalletTransactionService.getWalletTransaction(
                String.valueOf(refundId), PAYMENT_REFUND);
        if (refundTransaction != null) {
            throw exception(WALLET_REFUND_EXIST);
        }
        return walletTransaction.getWalletId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public PayWalletTransactionDO orderTransfer(String transferNo, Integer price, String subject) {
        // 1.1 判断退款单是否存在
        PayTransferDO payTransfer = payTransferService.getTransferByNo(transferNo);
        Assertions.notNull(payTransfer, "转账单据不存在");
        // 1.2 校验是否可以转账
        Long walletId = validateWalletCanTransfer(payTransfer.getId(), payTransfer.getChannelTransferNo());
        PayWalletDO wallet = payWalletMapper.selectById(walletId);
        Assert.notNull(wallet, "钱包 {} 不存在", walletId);

        // 2. 增加余额
        return addWalletBalance(walletId, String.valueOf(payTransfer.getId()), TRANSFER, price);
    }

    /**
     * 校验是否能转账
     *
     * @param transferId 转账单 id
     * @param walletPayNo 钱包支付 no
     */
    private Long validateWalletCanTransfer(Long transferId, String walletPayNo) {
        // 1. 校验钱包支付交易存在
        PayWalletTransactionDO walletTransaction = payWalletTransactionService.getWalletTransactionByNo(walletPayNo);
        Assertions.isFalse(walletTransaction == null, "未找到对应的钱包交易：" + walletPayNo);
        // 2. 校验转账是否存在
        PayWalletTransactionDO transferTransaction = payWalletTransactionService.getWalletTransaction(
                String.valueOf(transferId), TRANSFER);
        Assertions.isFalse(transferTransaction != null, "已存在钱包转账");
        return walletTransaction.getWalletId();
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    @SneakyThrows
    public PayWalletTransactionDO reduceWalletBalance(Long walletId, String bizId,
                                                      PayWalletBizTypeEnum bizType, Integer price) {
        int absPrice = Math.abs(price); //防止恶意传入负数
        // 1. 获取钱包
        PayWalletDO payWallet = getWallet(walletId);
        if (payWallet == null) {
            log.error("[reduceWalletBalance][用户钱包({})不存在]", walletId);
            throw exception(WALLET_NOT_FOUND);
        }
        final AtomicReference<BillTypeEnum> billType = new AtomicReference<>();
        // 2. 加锁，更新钱包余额（目的：避免钱包流水的并发更新时，余额变化不连贯）
        PayWalletTransactionDO transactionDO = lockRedisRepository.lock(String.format(PAY_WALLET_UPDATE_LOCK, walletId), UPDATE_TIMEOUT_MILLIS, () -> {
            // 2. 扣除余额
            int updateCounts = switch (bizType) {
                case PAYMENT -> {
                    billType.set(BillTypeEnum.UNKNOWN);
                    yield payWalletMapper.updateWhenConsumption(payWallet.getId(), absPrice);
                }
                case RECHARGE_REFUND -> {
                    billType.set(BillTypeEnum.RECHARGE_REFUND);
                    yield payWalletMapper.updateWhenRechargeRefund(payWallet.getId(), absPrice);
                }
                case UPDATE_BALANCE -> {
                    billType.set(BillTypeEnum.SYSTEM_ADJUST);
                    yield payWalletMapper.updateBalance(payWallet.getId(), -1 * absPrice);
                }
                case TRANSFER -> {
                    billType.set(BillTypeEnum.TRANSFER_OUTPUT);
                    yield payWalletMapper.updateBalance(payWallet.getId(), -1 * absPrice);
                }
                case CASH_OUT_APPLY -> {
                    billType.set(BillTypeEnum.CASH_OUT_SUCCESS);
                    yield payWalletMapper.updateWhenCashOut(payWallet.getId(), absPrice);
                }
                case COMMISSION_CANCEL -> {
                    billType.set(BillTypeEnum.COMMISSION_CANCEL);
                    yield payWalletMapper.updateMoney(payWallet.getId(), -1 * absPrice);
                }
                case REVENUE_CANCEL -> {
                    billType.set(BillTypeEnum.REVENUE_CANCEL);
                    yield payWalletMapper.updateMoney(payWallet.getId(), -1 * absPrice);
                }
                default -> throw new ServiceException(NOT_IMPLEMENTED.getCode(), "待实现");
            };
            if (updateCounts == 0) {
                throw new KnownServiceException(WALLET_BALANCE_NOT_ENOUGH);
            }

            // 3. 生成钱包流水
            Integer afterBalance = payWallet.getBalance() - absPrice;
            WalletTransactionCreateReqBO reqBO = new WalletTransactionCreateReqBO()
                    .setWalletId(payWallet.getId())
                    .setPrice(-1 * absPrice)
                    .setBalance(afterBalance)
                    .setBizId(bizId)
                    .setBizType(bizType.getValue())
                    .setTitle(bizType.getName());
            return payWalletTransactionService.createWalletTransaction(reqBO);
        });
        if (bizType == PAYMENT) {
            // 下单支付时已经记录过，此处不重复记录账单
            return transactionDO;
        }
        // 3.记录账单 TODO 后续重构，使用事件发布与监听机制解耦
        payBillService.expendAsync(payWallet.getUserId(), BillCategoryEnum.MONEY, billType.get(), 1d * absPrice, bizType.getName(), bizId, "{\"walletId\":\"%s\"}".formatted(walletId));
        return transactionDO;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @SneakyThrows
    public PayWalletTransactionDO addWalletBalance(Long walletId, String bizId,
                                                   PayWalletBizTypeEnum bizType, Integer price) {
        Assertions.isTrue(price >= 0, "钱包余额增加价格必须大于 0");
        // 1. 获取钱包
        PayWalletDO payWallet = getWallet(walletId);
        if (payWallet == null) {
            log.error("[addWalletBalance][用户钱包({})不存在]", walletId);
            throw exception(WALLET_NOT_FOUND);
        }
        final AtomicReference<BillTypeEnum> billType = new AtomicReference<>();
        // 2. 加锁，更新钱包余额（目的：避免钱包流水的并发更新时，余额变化不连贯）
        PayWalletTransactionDO transactionDO = lockRedisRepository.lock(String.format(PAY_WALLET_UPDATE_LOCK, walletId), UPDATE_TIMEOUT_MILLIS, () -> {
            // 3. 更新钱包金额
            switch (bizType) {
                case PAYMENT_REFUND:
                    billType.set(BillTypeEnum.PAY_PRODUCT_REFUND);
                    payWalletMapper.updateWhenConsumptionRefund(payWallet.getId(), price);
                    break;
                case RECHARGE:
                    billType.set(BillTypeEnum.RECHARGE);
                    payWalletMapper.updateWhenRecharge(payWallet.getId(), price);
                    break;
                case UPDATE_BALANCE:
                    billType.set(BillTypeEnum.SYSTEM_ADJUST);
                    payWalletMapper.updateBalance(payWallet.getId(), price);
                    break;
                case TRANSFER:
                    billType.set(BillTypeEnum.TRANSFER_INPUT);
                    payWalletMapper.updateBalance(payWallet.getId(), price);
                    break;
                case CRYPTOCURRENCY_ORIGIN_TO_MONEY:
                case CRYPTOCURRENCY_INCREMENT_TO_MONEY:
                case BONUS_TO_MONEY:
                case INTEREST_TO_MONEY:
                    billType.set(BillTypeEnum.EXCHANGE_MONEY);
                    payWalletMapper.updateMoney(payWallet.getId(), price);
                    break;
                case CASH_OUT_CLOSED:
                    billType.set(BillTypeEnum.CASH_OUT_FAILURE);
                    payWalletMapper.updateWhenCashOut(payWallet.getId(), -1 * price);
                    break;
                case COMMISSION_UNFREEZE:
                    billType.set(BillTypeEnum.COMMISSION_UNFREEZE);
                    payWalletMapper.updateMoney(payWallet.getId(), price);
                    break;
                case REVENUE_UNFREEZE:
                    billType.set(BillTypeEnum.REVENUE_UNFREEZE);
                    payWalletMapper.updateMoney(payWallet.getId(), price);
                    break;
                case REVENUE_GIFT:
                    billType.set(BillTypeEnum.REVENUE_GIFT);
                    payWalletMapper.updateMoney(payWallet.getId(), price);
                    break;
                default:
                    throw new UnsupportedOperationException("待实现：" + bizType);
            }
            // 4. 生成钱包流水
            WalletTransactionCreateReqBO transactionCreateReqBO = new WalletTransactionCreateReqBO()
                    .setWalletId(payWallet.getId())
                    .setPrice(bizType == PayWalletBizTypeEnum.CASH_OUT_CLOSED ? -1 * price : price)
                    .setBalance(payWallet.getBalance() + price)
                    .setBizId(bizId)
                    .setBizType(bizType.getValue())
                    .setTitle(bizType.getName());
            return payWalletTransactionService.createWalletTransaction(transactionCreateReqBO);
        });
        // 3. 记录账单 TODO 后续重构，使用事件发布与监听机制解耦
        payBillService.incomeAsync(payWallet.getUserId(), BillCategoryEnum.MONEY, billType.get(), 1d * price, bizType.getName(), bizId, "{\"walletId\":\"%s\"}".formatted(walletId));
        return transactionDO;
    }

    @Override
    public void freezePrice(Long id, Integer price) {
        int updateCounts = payWalletMapper.freezePrice(id, price);
        if (updateCounts == 0) {
            throw new KnownServiceException(WALLET_BALANCE_NOT_ENOUGH);
        }
    }

    @Override
    public void unfreezePrice(Long id, Integer price) {
        int updateCounts = payWalletMapper.unfreezePrice(id, price);
        if (updateCounts == 0) {
            throw new KnownServiceException(WALLET_FREEZE_PRICE_NOT_ENOUGH);
        }
    }

    @Override
    public void resetMoneyCashing(Long userId, Integer userType, Integer applyPrice) {
        PayWalletDO walletDO = getOrCreateWallet(userId, userType);
        PayWalletDO updateObj = new PayWalletDO();
        updateObj.setId(walletDO.getId());
        updateObj.setMoneyCashing(walletDO.getMoneyCashing() - Math.abs(applyPrice));
        payWalletMapper.updateById(updateObj);
    }

}
